package com.practice.security.pkpass;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;


public class PassSigner {
	
	/**
	 * Public Key Cryptography Standards #12:<br>
	 * Personal Information Exchange Syntax Standard
	 */
	public static final String CRYPTOGRAPHY_PKCS12 = "PKCS12";
	
	/** X.509 certificate */
	public static final String CRYTOGRAPHY_X509 = "X.509";
	
	/** SHA1 with RSA */
	public static final String SIGNER_TYPE_SHA1_RSA = "SHA1withRSA";

	/** Unicode Transformation Format 8-bit */
	public static final String ENCODE_UTF8 = "UTF-8";
	
	private static final String SIGNER_PRIVATE_KEY_PATH = "Certificate_with_Key.p12";
	
	private static final String SIGNER_PRIVATE_KEY_PASSWORD = "mx73lkjx";
	
	private static final String WWDR_PUBLIC_KEY_PATH = "AppleWWDRCA.cer";

	private PrivateKey signerPrivateKey = null;

	private X509Certificate signerCertificate = null;

	private X509Certificate wwdrCertificate = null;

	private CMSSignedDataGenerator cmsSignedDataGenerator = null;

	public PassSigner() {
		
	}

	public byte[] signManifest(String manifestJson) throws IOException {
		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
			Security.addProvider(new BouncyCastleProvider());
		}

		/*
		 * Load signer private key from P12 file.
		 */
		if (signerPrivateKey == null || signerCertificate == null) {
			loadSignerPrivateKey();
		}

		/*
		 * Load WWDR public key.
		 */
		if (wwdrCertificate == null) {
			loadPublicKey();
		}

		/*
		 * Create CMSSignedDataGenerator, set certificates.
		 */
		if (cmsSignedDataGenerator == null) {
			createSignedDataGenerator();
		}

		CMSProcessableByteArray cmsProcessableByteArray =
				new CMSProcessableByteArray(manifestJson.getBytes(ENCODE_UTF8));

		CMSSignedData cmsSignedData = null;
		try {
			cmsSignedData = cmsSignedDataGenerator.generate(cmsProcessableByteArray, false);
		} catch (CMSException e) {
			throw new IOException("Encrypt manifest JSON failed.", e);
		}

		return cmsSignedData.getEncoded();
	}
	
	private void loadSignerPrivateKey() throws IOException {
		KeyStore pkcs12KeyStore;
		try {
			pkcs12KeyStore =
					KeyStore.getInstance(CRYPTOGRAPHY_PKCS12, BouncyCastleProvider.PROVIDER_NAME);
		} catch (NoSuchProviderException | KeyStoreException e) {
			throw new IOException("Create key store failed.", e);
		}

		File signerPrivateKeyFile = new File(SIGNER_PRIVATE_KEY_PATH);
		String signerPrivateKeyPassword = SIGNER_PRIVATE_KEY_PASSWORD;

		Enumeration<String> aliases;
		try (InputStream signerPrivateKeyInput = new FileInputStream(signerPrivateKeyFile)) {
			pkcs12KeyStore.load(signerPrivateKeyInput, signerPrivateKeyPassword.toCharArray());
			aliases = pkcs12KeyStore.aliases();
		} catch (NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
			throw new IOException("Load private key failed.", e);
		}

		try {
			while (aliases.hasMoreElements()) {
				String alias = aliases.nextElement();

				Key key = pkcs12KeyStore.getKey(alias, signerPrivateKeyPassword.toCharArray());
				if (key instanceof PrivateKey) {
					PrivateKey privateKey = (PrivateKey) key;
					signerPrivateKey = privateKey;

					Certificate certificate = pkcs12KeyStore.getCertificate(alias);
					if (certificate instanceof X509Certificate) {
						signerCertificate = (X509Certificate) certificate;
						break;
					}
				}
			}
		} catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
			throw new IOException("Get private key and certificate failed.", e);
		}

		if (signerPrivateKey == null || signerCertificate == null) {
			throw new IOException("Private key or certificate is not found.");
		}
	}

	private void loadPublicKey() throws IOException {
		File wwdrPublicKeyFile = new File(WWDR_PUBLIC_KEY_PATH);

		try (InputStream wwdrPublicKeyFileInput = new FileInputStream(wwdrPublicKeyFile)) {
			CertificateFactory certificateFactory = CertificateFactory.getInstance(CRYTOGRAPHY_X509,
					BouncyCastleProvider.PROVIDER_NAME);
			Certificate certificate = certificateFactory.generateCertificate(wwdrPublicKeyFileInput);
			if (certificate instanceof X509Certificate) {
				wwdrCertificate = (X509Certificate) certificate;
			}
		} catch (CertificateException | NoSuchProviderException e) {
			throw new IOException("Load public key failed.", e);
		}

		if (wwdrCertificate == null) {
			throw new IOException("Public key cannot be decrypted.");
		}
	}

	private void createSignedDataGenerator() throws IOException {
		cmsSignedDataGenerator = new CMSSignedDataGenerator();
		try {
			ContentSigner sha1Signer = new JcaContentSignerBuilder(SIGNER_TYPE_SHA1_RSA).setProvider(
					BouncyCastleProvider.PROVIDER_NAME).build(signerPrivateKey);

			DigestCalculatorProvider digestCalculatorProvider =
					new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build();

			SignerInfoGenerator signerInfoGenerator =
					new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider).build(sha1Signer, signerCertificate);

			cmsSignedDataGenerator.addSignerInfoGenerator(signerInfoGenerator);

			List<Certificate> certificates = new ArrayList<>();
			certificates.add(wwdrCertificate);
			certificates.add(signerCertificate);

			JcaCertStore jcaCertStore = new JcaCertStore(certificates);
			cmsSignedDataGenerator.addCertificates(jcaCertStore);
		} catch (OperatorCreationException | CertificateEncodingException | CMSException e) {
			throw new IOException("Create CMSSignedDataGenerator failed.", e);
		}
	}
	
}
